package org.hilo.boot.app.component;

import java.io.IOException;
import java.io.InputStream;
import java.io.PushbackInputStream;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;

import javax.servlet.http.HttpServletRequest;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.util.StreamUtils;
import org.springframework.util.StringUtils;

/**
 * 
 * @author zollty
 * @since 2020年11月6日
 */
public class RequestBodyParser {
    // 除POST之外，其他三种属于Spring的特殊扩展。
    private static final List<String> FORM_METHODS = Arrays.asList("POST", "PUT", "PATCH", "DELETE");
    // 暂不支持DELETE、GET传递Body数据，根据 RFC7231规范（https://tools.ietf.org/html/rfc7231）的建议。
    // 其他几个特殊用途的方法，当然也不需要解析Body，包括：HEAD  OPTIONS CONNECT TRACE
    // 剩下的常用就只有： POST PUT，不常用的方法：PATCH, LINK, UNLINK （参见rfc2616），目前支持 PATCH
    private static final List<String> INCLUDE_METHODS = Arrays.asList("POST", "PUT", "PATCH");
    private static final Logger logger = LoggerFactory.getLogger(RequestBodyParser.class);
    /**
     * The default charset used by the converter.
     */
    private Charset charset = StandardCharsets.UTF_8;

    public void setCharset(Charset charset) {
        this.charset = charset;
    }

    public String getBodyIfNecessary(HttpServletRequest request) {

        HttpInputMessage inputMessage = new ServletServerHttpRequest(request) {
            @Override
            public InputStream getBody() throws IOException {
                return request.getInputStream();
            }
        };

        String body = null;
        if (shouldParseBody(request)) {
            try {
                body = read(inputMessage);
            } catch (IOException e) {
                if (logger.isDebugEnabled()) {
                    logger.info("can not parse body to string, uri=" + request.getRequestURI(), e);
                }
            }
        }
        return body;
    }
    
    private boolean shouldParseBody(HttpServletRequest request) {
        String contentType = request.getContentType();
        String method = request.getMethod();
        if (StringUtils.hasLength(contentType) && INCLUDE_METHODS.contains(method)) {
            try {
                MediaType mediaType = MediaType.parseMediaType(contentType);
                return MediaType.APPLICATION_JSON.includes(mediaType)
                        || MediaType.APPLICATION_XML.includes(mediaType)
                        || MediaType.APPLICATION_FORM_URLENCODED.includes(mediaType);
            } catch (IllegalArgumentException ex) {
            }
        }
        return false;
    }

    protected boolean isPlainFormPostRequest(HttpServletRequest request) {
        String contentType = request.getContentType();
        String method = request.getMethod();
        if (StringUtils.hasLength(contentType) && FORM_METHODS.contains(method)) {
            try {
                MediaType mediaType = MediaType.parseMediaType(contentType);
                return MediaType.APPLICATION_FORM_URLENCODED.includes(mediaType);
            } catch (IllegalArgumentException ex) {
            }
        }
        return false;
    }

    public String read(HttpInputMessage inputMessage) throws IOException {

        MediaType contentType = inputMessage.getHeaders().getContentType();
        Charset charset = (contentType != null && contentType.getCharset() != null
                ? contentType.getCharset()
                : this.charset);
        return StreamUtils.copyToString(getInputStream(inputMessage), charset);
    }
    
    public InputStream getInputStream(HttpInputMessage inputMessage) throws IOException {
        InputStream body;
        InputStream inputStream = inputMessage.getBody();
        if (inputStream.markSupported()) {
            inputStream.mark(1);
            body = (inputStream.read() != -1 ? inputStream : null);
            inputStream.reset();
        }
        else {
            PushbackInputStream pushbackInputStream = new PushbackInputStream(inputStream);
            int b = pushbackInputStream.read();
            if (b == -1) {
                body = null;
            }
            else {
                body = pushbackInputStream;
                pushbackInputStream.unread(b);
            }
        }
        return body;
    }

}
